(function(global, factory) {
  if (typeof define === 'function' && define.amd) {
    define([], factory);
  } else if (typeof module !== 'undefined' && module.exports) {
    module.exports = factory();
  } else {
    global.ReconnectingWebSocket = factory();
  }
})(this, function() {

  if (!('WebSocket' in window)) {
    return;
  }

  function ReconnectingWebSocket(url, protocols, options) {

    var settings = {

      debug: false,

      automaticOpen: true,

      reconnectInterval: 1000,
      maxReconnectInterval: 30000,
      reconnectDecay: 1,
      timeoutInterval: 2000,

      maxReconnectAttempts: null,

      binaryType: 'blob'
    }
    if (!options) {
      options = {};
    }

    for (var key in settings) {
      if (typeof options[key] !== 'undefined') {
        this[key] = options[key];
      } else {
        this[key] = settings[key];
      }
    }


    this.url = url;

    this.reconnectAttempts = 0;

    this.readyState = WebSocket.CONNECTING;

    this.protocol = null;


    var self = this;
    var ws;
    var forcedClose = false;
    var timedOut = false;
    var eventTarget = document.createElement('div');


    eventTarget.addEventListener('open', function(event) {
      self.onopen(event);
    });
    eventTarget.addEventListener('close', function(event) {
      self.onclose(event);
    });
    eventTarget.addEventListener('connecting', function(event) {
      self.onconnecting(event);
    });
    eventTarget.addEventListener('message', function(event) {
      self.onmessage(event);
    });
    eventTarget.addEventListener('error', function(event) {
      self.onerror(event);
    });


    this.addEventListener = eventTarget.addEventListener.bind(eventTarget);
    this.removeEventListener = eventTarget.removeEventListener.bind(eventTarget);
    this.dispatchEvent = eventTarget.dispatchEvent.bind(eventTarget);

    function generateEvent(s, args) {
      var evt = document.createEvent("CustomEvent");
      evt.initCustomEvent(s, false, false, args);
      return evt;
    }
    ;

    this.open = function(reconnectAttempt) {
      ws = new WebSocket(self.url, protocols || []);
      ws.binaryType = this.binaryType;

      if (reconnectAttempt) {
        if (this.maxReconnectAttempts && this.reconnectAttempts > this.maxReconnectAttempts) {
          return;
        }
      } else {
        eventTarget.dispatchEvent(generateEvent('connecting'));
        this.reconnectAttempts = 0;
      }

      if (self.debug || ReconnectingWebSocket.debugAll) {
        console.debug('ReconnectingWebSocket', 'attempt-connect', self.url);
      }

      var localWs = ws;
      var timeout = setTimeout(function() {
        if (self.debug || ReconnectingWebSocket.debugAll) {
          console.debug('ReconnectingWebSocket', 'connection-timeout', self.url);
        }
        timedOut = true;
        localWs.close();
        timedOut = false;
      }, self.timeoutInterval);

      ws.onopen = function(event) {
        clearTimeout(timeout);
        if (self.debug || ReconnectingWebSocket.debugAll) {
          console.debug('ReconnectingWebSocket', 'onopen', self.url);
        }
        self.protocol = ws.protocol;
        self.readyState = WebSocket.OPEN;
        self.reconnectAttempts = 0;
        var e = generateEvent('open');
        e.isReconnect = reconnectAttempt;
        reconnectAttempt = false;
        eventTarget.dispatchEvent(e);
      };

      ws.onclose = function(event) {
        clearTimeout(timeout);
        ws = null;
        if (forcedClose) {
          self.readyState = WebSocket.CLOSED;
          eventTarget.dispatchEvent(generateEvent('close'));
        } else {
          self.readyState = WebSocket.CONNECTING;
          var e = generateEvent('connecting');
          e.code = event.code;
          e.reason = event.reason;
          e.wasClean = event.wasClean;
          eventTarget.dispatchEvent(e);
          if (!reconnectAttempt && !timedOut) {
            if (self.debug || ReconnectingWebSocket.debugAll) {
              console.debug('ReconnectingWebSocket', 'onclose', self.url);
            }
            eventTarget.dispatchEvent(generateEvent('close'));
          }

          var timeout = self.reconnectInterval * Math.pow(self.reconnectDecay, self.reconnectAttempts);
          setTimeout(function() {
            self.reconnectAttempts++;
            self.open(true);
          }, timeout > self.maxReconnectInterval ? self.maxReconnectInterval : timeout);
        }
      };
      ws.onmessage = function(event) {
        if (self.debug || ReconnectingWebSocket.debugAll) {
          console.debug('ReconnectingWebSocket', 'onmessage', self.url, event.data);
        }
        var e = generateEvent('message');
        e.data = event.data;
        eventTarget.dispatchEvent(e);
      };
      ws.onerror = function(event) {
        if (self.debug || ReconnectingWebSocket.debugAll) {
          console.debug('ReconnectingWebSocket', 'onerror', self.url, event);
        }
        eventTarget.dispatchEvent(generateEvent('error'));
      };
    }

    if (this.automaticOpen == true) {
      this.open(false);
    }

    this.send = function(data) {
      if (ws) {
        if (self.debug || ReconnectingWebSocket.debugAll) {
          console.debug('ReconnectingWebSocket', 'send', self.url, data);
        }
        return ws.send(data);
      } else {
        throw 'INVALID_STATE_ERR : Pausing to reconnect websocket';
      }
    };

    this.close = function(code, reason) {
      if (typeof code == 'undefined') {
        code = 1000;
      }
      forcedClose = true;
      if (ws) {
        ws.close(code, reason);
      }
    };

    this.refresh = function() {
      if (ws) {
        ws.close();
      }
    };
  }

  ReconnectingWebSocket.prototype.onopen = function(event) {};
  ReconnectingWebSocket.prototype.onclose = function(event) {};
  ReconnectingWebSocket.prototype.onconnecting = function(event) {};
  ReconnectingWebSocket.prototype.onmessage = function(event) {};
  ReconnectingWebSocket.prototype.onerror = function(event) {};
  ReconnectingWebSocket.debugAll = false;

  ReconnectingWebSocket.CONNECTING = WebSocket.CONNECTING;
  ReconnectingWebSocket.OPEN = WebSocket.OPEN;
  ReconnectingWebSocket.CLOSING = WebSocket.CLOSING;
  ReconnectingWebSocket.CLOSED = WebSocket.CLOSED;

  return ReconnectingWebSocket;
});
